]>
Commit | Line | Data |
---|---|---|
be897af3 RBR |
1 | // Copyright (C) 2024 Rubén Beltrán del Río |
2 | ||
3 | // This program is free software: you can redistribute it and/or modify | |
4 | // it under the terms of the GNU General Public License as published by | |
5 | // the Free Software Foundation, either version 3 of the License, or | |
6 | // (at your option) any later version. | |
7 | ||
8 | // This program is distributed in the hope that it will be useful, | |
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | // GNU General Public License for more details. | |
12 | ||
13 | // You should have received a copy of the GNU General Public License | |
14 | // along with this program. If not, see https://map.tranquil.systems. | |
5e8ff485 RBR |
15 | import Cocoa |
16 | import SwiftUI | |
17 | ||
18 | class MapTextEditorController: NSViewController { | |
19 | ||
e2c37ac1 | 20 | @Binding var document: MapDocument |
14491563 RBR |
21 | var highlightRanges: [Range<String.Index>] { |
22 | didSet { | |
23 | updateHighlights() | |
24 | } | |
25 | } | |
26 | ||
27 | var selectedRange: Int { | |
28 | didSet { | |
29 | updateHighlights() | |
30 | focusOnResult() | |
31 | } | |
32 | } | |
33 | ||
fdb4633d | 34 | let onChange: () -> Void |
5e8ff485 | 35 | |
77d0155b RBR |
36 | private let vertexRegex = MapParsingPatterns.vertex |
37 | private let edgeRegex = MapParsingPatterns.edge | |
38 | private let blockerRegex = MapParsingPatterns.blocker | |
39 | private let opportunityRegex = MapParsingPatterns.opportunity | |
fdb4633d | 40 | private let noteRegex = MapParsingPatterns.note |
77d0155b | 41 | private let stageRegex = MapParsingPatterns.stage |
e2c37ac1 | 42 | private let groupRegex = MapParsingPatterns.group |
77d0155b | 43 | |
fdb4633d | 44 | private let changeDebouncer: Debouncer = Debouncer(seconds: 1) |
77d0155b | 45 | |
14491563 RBR |
46 | init( |
47 | document: Binding<MapDocument>, highlightRanges: [Range<String.Index>], selectedRange: Int, | |
48 | onChange: @escaping () -> Void | |
49 | ) { | |
e2c37ac1 | 50 | self._document = document |
fdb4633d | 51 | self.onChange = onChange |
14491563 RBR |
52 | self.highlightRanges = highlightRanges |
53 | self.selectedRange = selectedRange | |
5e8ff485 RBR |
54 | super.init(nibName: nil, bundle: nil) |
55 | } | |
56 | ||
57 | required init?(coder: NSCoder) { | |
58 | fatalError("init(coder:) has not been implemented") | |
59 | } | |
60 | ||
61 | override func loadView() { | |
62 | let scrollView = NSTextView.scrollableTextView() | |
63 | let textView = scrollView.documentView as! NSTextView | |
64 | ||
65 | scrollView.translatesAutoresizingMaskIntoConstraints = false | |
66 | ||
be897af3 | 67 | textView.backgroundColor = .UI.background |
75a0e450 | 68 | textView.allowsUndo = true |
5e8ff485 | 69 | textView.delegate = self |
77d0155b | 70 | textView.textStorage?.delegate = self |
e2c37ac1 | 71 | textView.string = self.document.text |
5e8ff485 RBR |
72 | textView.isEditable = true |
73 | textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular) | |
74 | self.view = scrollView | |
75 | } | |
76 | ||
77 | override func viewDidAppear() { | |
78 | self.view.window?.makeFirstResponder(self.view) | |
14491563 RBR |
79 | updateHighlights() |
80 | } | |
81 | ||
82 | private var textView: NSTextView? { | |
83 | return (view as? NSScrollView)?.documentView as? NSTextView | |
84 | } | |
85 | ||
86 | private func updateHighlights() { | |
87 | if let textView { | |
88 | if let textStorage = textView.textStorage { | |
89 | textStorage.removeAttribute( | |
90 | .backgroundColor, range: NSRange(location: 0, length: textStorage.length)) | |
91 | ||
92 | for range in highlightRanges { | |
93 | let nsRange = NSRange(range, in: textStorage.string) | |
94 | ||
be897af3 | 95 | textStorage.addAttribute(.backgroundColor, value: NSColor.Syntax.match, range: nsRange) |
14491563 RBR |
96 | } |
97 | ||
98 | textView.needsDisplay = true | |
99 | ||
100 | } | |
101 | } | |
102 | } | |
103 | ||
104 | private func focusOnResult() { | |
105 | if let textView { | |
106 | if let textStorage = textView.textStorage { | |
107 | if selectedRange < highlightRanges.count { | |
108 | let range = highlightRanges[selectedRange] | |
109 | let nsRange = NSRange(range, in: textStorage.string) | |
110 | textView.scrollRangeToVisible(nsRange) | |
111 | textView.selectedRange = nsRange | |
112 | } | |
113 | } | |
114 | } | |
115 | } | |
116 | ||
117 | private func setSelectionColor() { | |
118 | guard let textView = self.textView else { return } | |
119 | ||
120 | var selectedTextAttributes = textView.selectedTextAttributes | |
121 | selectedTextAttributes[.backgroundColor] = NSColor.yellow.withAlphaComponent(0.3) | |
122 | textView.selectedTextAttributes = selectedTextAttributes | |
5e8ff485 RBR |
123 | } |
124 | } | |
125 | ||
126 | extension MapTextEditorController: NSTextViewDelegate { | |
127 | ||
128 | func textDidChange(_ obj: Notification) { | |
129 | if let textField = obj.object as? NSTextView { | |
e2c37ac1 RBR |
130 | self.document.text = textField.string |
131 | ||
132 | changeDebouncer.debounce { | |
133 | DispatchQueue.main.async { | |
134 | self.onChange() | |
fdb4633d | 135 | } |
e2c37ac1 | 136 | } |
5e8ff485 RBR |
137 | } |
138 | } | |
139 | ||
140 | func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool | |
141 | { | |
142 | let range = Range(shouldChangeTextIn, in: view.string) | |
143 | let target = view.string[range!] | |
144 | ||
145 | if target == "--" { | |
146 | return false | |
147 | } | |
148 | ||
149 | return true | |
150 | } | |
151 | } | |
152 | ||
77d0155b | 153 | extension MapTextEditorController: NSTextStorageDelegate { |
fdb4633d | 154 | |
77d0155b RBR |
155 | override func textStorageDidProcessEditing(_ obj: Notification) { |
156 | if let textStorage = obj.object as? NSTextStorage { | |
fdb4633d | 157 | self.colorizeText(textStorage: textStorage) |
77d0155b RBR |
158 | } |
159 | } | |
160 | ||
161 | private func colorizeText(textStorage: NSTextStorage) { | |
162 | let range = NSMakeRange(0, textStorage.length) | |
163 | var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range) | |
77d0155b RBR |
164 | |
165 | for match in matches { | |
e2c37ac1 | 166 | textStorage.addAttributes( |
be897af3 | 167 | [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 1)) |
e2c37ac1 | 168 | textStorage.addAttributes( |
be897af3 | 169 | [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 2)) |
e2c37ac1 | 170 | textStorage.addAttributes( |
be897af3 | 171 | [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 3)) |
e2c37ac1 | 172 | textStorage.addAttributes( |
be897af3 | 173 | [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 4)) |
77d0155b RBR |
174 | } |
175 | ||
176 | matches = edgeRegex.matches(in: textStorage.string, options: [], range: range) | |
177 | ||
178 | for match in matches { | |
e2c37ac1 | 179 | textStorage.addAttributes( |
be897af3 | 180 | [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 1)) |
77d0155b RBR |
181 | let arrowRange = match.range(at: 2) |
182 | textStorage.addAttributes( | |
be897af3 | 183 | [.foregroundColor: NSColor.Syntax.symbol], |
77d0155b | 184 | range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1)) |
e2c37ac1 | 185 | textStorage.addAttributes( |
be897af3 | 186 | [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 3)) |
77d0155b RBR |
187 | } |
188 | ||
189 | matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range) | |
190 | ||
191 | for match in matches { | |
e2c37ac1 | 192 | textStorage.addAttributes( |
be897af3 | 193 | [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 1)) |
e2c37ac1 | 194 | textStorage.addAttributes( |
be897af3 | 195 | [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 2)) |
e2c37ac1 | 196 | textStorage.addAttributes( |
be897af3 | 197 | [.foregroundColor: NSColor.Syntax.symbol], range: match.range(at: 3)) |
e2c37ac1 | 198 | textStorage.addAttributes( |
be897af3 | 199 | [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 4)) |
77d0155b RBR |
200 | } |
201 | ||
202 | matches = blockerRegex.matches(in: textStorage.string, options: [], range: range) | |
203 | ||
204 | for match in matches { | |
e2c37ac1 | 205 | textStorage.addAttributes( |
be897af3 | 206 | [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 1)) |
e2c37ac1 | 207 | textStorage.addAttributes( |
be897af3 | 208 | [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 2)) |
fdb4633d | 209 | } |
e2c37ac1 | 210 | |
fdb4633d RBR |
211 | matches = noteRegex.matches(in: textStorage.string, options: [], range: range) |
212 | ||
213 | for match in matches { | |
e2c37ac1 | 214 | textStorage.addAttributes( |
be897af3 | 215 | [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 1)) |
e2c37ac1 | 216 | textStorage.addAttributes( |
be897af3 | 217 | [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 2)) |
e2c37ac1 | 218 | textStorage.addAttributes( |
be897af3 | 219 | [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 3)) |
77d0155b RBR |
220 | } |
221 | ||
222 | matches = stageRegex.matches(in: textStorage.string, options: [], range: range) | |
223 | ||
224 | for match in matches { | |
e2c37ac1 | 225 | textStorage.addAttributes( |
be897af3 | 226 | [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 1)) |
e2c37ac1 | 227 | textStorage.addAttributes( |
be897af3 | 228 | [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 2)) |
e2c37ac1 RBR |
229 | } |
230 | ||
231 | matches = groupRegex.matches(in: textStorage.string, options: [], range: range) | |
232 | ||
233 | for match in matches { | |
234 | textStorage.addAttributes( | |
be897af3 | 235 | [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 1)) |
e2c37ac1 | 236 | textStorage.addAttributes( |
be897af3 | 237 | [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 2)) |
77d0155b RBR |
238 | } |
239 | } | |
240 | } | |
241 | ||
5e8ff485 RBR |
242 | struct MapTextEditor: NSViewControllerRepresentable { |
243 | ||
e2c37ac1 | 244 | @Binding var document: MapDocument |
14491563 RBR |
245 | var highlightRanges: [Range<String.Index>] |
246 | var selectedRange: Int | |
fdb4633d | 247 | var onChange: () -> Void = {} |
5e8ff485 RBR |
248 | |
249 | func makeNSViewController( | |
250 | context: NSViewControllerRepresentableContext<MapTextEditor> | |
251 | ) -> MapTextEditorController { | |
14491563 RBR |
252 | return MapTextEditorController( |
253 | document: $document, highlightRanges: highlightRanges, selectedRange: selectedRange, | |
254 | onChange: onChange) | |
5e8ff485 RBR |
255 | } |
256 | ||
257 | func updateNSViewController( | |
258 | _ nsViewController: MapTextEditorController, | |
259 | context: NSViewControllerRepresentableContext<MapTextEditor> | |
14491563 RBR |
260 | ) { |
261 | nsViewController.highlightRanges = highlightRanges | |
262 | nsViewController.selectedRange = selectedRange | |
263 | } | |
5e8ff485 | 264 | } |